package com.gmrz.webauthn.handler;

import co.nstant.in.cbor.model.DataItem;
import com.gmrz.service.challengestore.ChallengeStoreService;
import com.gmrz.uaf.common.Constants;
import com.gmrz.uaf.common.GuiceUtil;
import com.gmrz.uaf.crypto.CryptoEngine;
import com.gmrz.uaf.db.DAOException;
import com.gmrz.uaf.db.UAFDAOFactory;
import com.gmrz.uaf.db.UAFDBConnectionMgr;
import com.gmrz.uaf.db.dao.DevicesDao;
import com.gmrz.uaf.protocol.v1.processor.exception.PolicyNotFoundException;
import com.gmrz.uaf.protocol.v1.processor.exception.UAFErrorCode;
import com.gmrz.uaf.protocol.v1.schema.DeviceDetails;
import com.gmrz.uaf.servlet.v1.handler.UAFBaseHandler;
import com.gmrz.uaf.servlet.v1.validation.PayloadValidationException;
import com.gmrz.uaf.servlet.v1.validation.RestValidator;
import com.gmrz.util.Convert;
import com.gmrz.util.CryUtil;
import com.gmrz.util.Strings;
import com.gmrz.util.UUIDGenerator;
import com.gmrz.util.db.DBUtil;
import com.gmrz.webauthn.common.CBORDecodeUtil;
import com.gmrz.webauthn.common.UserAgentUtil;
import com.gmrz.webauthn.common.WebAuthnConstants;
import com.gmrz.webauthn.db.dao.WebAuthnAuthenticatorsDAO;
import com.gmrz.webauthn.db.dao.WebAuthnMetadataDAO;
import com.gmrz.webauthn.protocol.v1.json.WebAuthnSchemaBuilder;
import com.gmrz.webauthn.protocol.v1.processor.WebAuthnErrorCode;
import com.gmrz.webauthn.protocol.v1.processor.WebAuthnException;
import com.gmrz.webauthn.protocol.v1.processor.attestation.AndroidKeyAttestationProcessor;
import com.gmrz.webauthn.protocol.v1.processor.attestation.PackedAttestationProcessor;
import com.gmrz.webauthn.protocol.v1.processor.attestation.U2FAttestationProcessor;
import com.gmrz.webauthn.protocol.v1.schema.*;
import com.gmrz.webauthn.protocol.v1.validation.CollectedClientDataValidator;
import com.gmrz.webauthn.request.*;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import cz.mallat.uasparser.UserAgentInfo;
import org.apache.commons.codec.binary.Base64;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Set;

public class WebRegResponseHandler extends UAFBaseHandler {
    private static final Logger LOG = LogManager.getLogger(WebRegResponseHandler.class);

    public String regFinish(String request) {

        LOG.info("[/reg/send][begin]");
        Gson gson = WebAuthnSchemaBuilder.getGson();
        WebAuthnRespPayload<WebAuthnRequestContext> respPayload = new WebAuthnRespPayload<WebAuthnRequestContext>();
        Type reqType = new TypeToken<WebAuthnReqPayload<WebAuthnRegContext>>() {
        }.getType();

        String response;
        Connection conn = null;
        Connection saveConn = null;
        try {
            WebAuthnReqPayload<WebAuthnRegContext> reqPayload = gson.fromJson(request, reqType);
            //记录日志
            logRequest(request, reqPayload.getContext().getTransNo(), reqPayload.getContext().getUserName(),
                    reqPayload.getContext().getAppID(), reqPayload.getContext().getAuthType(), reqPayload.getContext().getTransType(), Constants.Method.REG_FINISH);
            DevicesContext browesDetails = reqPayload.getContext().getDevices();
            //验证请求参数
            validate(reqPayload);

            String rpID = WebAuthnConstants.getTenantConfigContent(WebAuthnConstants.WEBAUTHN_AUTHENTICATOR_RPID, reqPayload.getContext().getAppID());
            String rpName = WebAuthnConstants.getTenantConfigContent(WebAuthnConstants.WEBAUTHN_AUTHENTICATOR_RPNAME, reqPayload.getContext().getAppID());

            String rpIDSha256 = Convert.toBase64(CryUtil.getSHA256(rpID.getBytes()));

            String appID = reqPayload.getContext().getAppID();
            String authType = reqPayload.getContext().getAuthType();
            String transType = reqPayload.getContext().getTransType();

            String attachment = serverConfig.getPolicyConfig(appID,authType,transType,WebAuthnConstants.WEBAUTHN_AUTHENTICATOR_SELECTION_CRITERIA_AUTHENTICATOR_ATTACHMENT);
            String anonymous = serverConfig.getPolicyConfig(appID,authType,transType,WebAuthnConstants.WEBAUTHN_AUTH_ANONYMOUS);
            String strictmode = serverConfig.getPolicyConfig(appID,authType,transType,WebAuthnConstants.WEBAUTHN_AUTH_STRICTMODE);
            String residentkey = serverConfig.getPolicyConfig(appID,authType,transType,WebAuthnConstants.WEBAUTHN_AUTHENTICATOR_SELECTION_CRITERIA_REQUIRE_RESIDENTKEY);
            String verification = serverConfig.getPolicyConfig(appID,authType,transType,WebAuthnConstants.WEBAUTHN_AUTHENTICATOR_SELECTION_CRITERIA_USER_VERIFICATION);
            String timeout = serverConfig.getPolicyConfig(appID,authType,transType,WebAuthnConstants.WEBAUTHN_AUTHENTICATOR_TIME_OUT);
            String extensions = serverConfig.getPolicyConfig(appID,authType,transType,WebAuthnConstants.WEBAUTHN_AUTHENTICATOR_EXTENSIONS);


            List<DataItem> dataitems = CBORDecodeUtil.cborDecode(Base64.decodeBase64(reqPayload.getCredentials().getResponse().getAttestationObject()));
            co.nstant.in.cbor.model.Map cborMap = (co.nstant.in.cbor.model.Map) dataitems.get(0);

            java.util.Map<String, Object> map = CBORDecodeUtil.cborMapConversion(cborMap);

            byte[] rawAuthData = (byte[]) map.get("authData");

            AuthenticatorData authenticatorData = GuiceUtil.getProcessorInjector().getInstance(AuthenticatorData.class);
            authenticatorData.decode(rawAuthData);


            String formatName = (String) map.get("fmt");
            AttestationFormat format = AttestationFormat.forName(formatName);

            Object attStmt = map.get("attStmt");

            CredentialAttestation credentialAttestation = new CredentialAttestation().withAuthenticatorData(authenticatorData).withAuthenticatorDataBytes(rawAuthData).withAttestationStatement(attStmt).withFormat(format);
            credentialAttestation.setAuthenticatorDataBytes(rawAuthData);


            String clientDataStr = reqPayload.getCredentials().getResponse().getClientDataJSON();
            String clientDataDecodedStr = new String(Base64.decodeBase64(clientDataStr), Charset.forName(Constants.UTF_8));
            CollectedClientData clientData = WebAuthnSchemaBuilder.getGson().fromJson(clientDataDecodedStr, CollectedClientData.class);

            String serverData = new String(Convert.fromBase64(reqPayload.getServerData()));
            Type optionsType = new TypeToken<PublicKeyCredentialCreationOptions>() {
            }.getType();
            PublicKeyCredentialCreationOptions options = gson.fromJson(serverData, optionsType);

            //对报文中的挑战值进行校验
            ChallengeStoreService challengeStoreService = GuiceUtil.getProcessorInjector().getInstance(ChallengeStoreService.class);
            String challengeContent = challengeStoreService.getRedisValue(options.getChallenge());
            if (Strings.isNullOrEmpty(challengeContent)) {
                throw new WebAuthnException(WebAuthnErrorCode.STATUS_CHALLENGE_VALIDATION_FAILED);
            }

            CollectedClientDataValidator collectedClientDataValidator = new CollectedClientDataValidator(this.getServerConfig(), Constants.Operation.REG,reqPayload.getContext().getAppID(),options.getChallenge());
            collectedClientDataValidator.validate(clientData);


            authenticatorData = credentialAttestation.getAuthenticatorData();


            WebAuthenticator authenticator = GuiceUtil.getProcessorInjector().getInstance(WebAuthenticator.class);
            authenticator.setWaId(UUIDGenerator.generateID());

            AAGUID aaguid = authenticatorData.getAttestation().getAaguid();

            String authDataRpIDSha256 = Convert.toBase64(authenticatorData.getRpID());
            if(!rpIDSha256.equals(authDataRpIDSha256)){
                LOG.error("Failed to validate rpID hash in AuthenticatorData. AuthDataRpIDSha256:"+authDataRpIDSha256+",rpIDSha256:"+rpIDSha256);
                throw new WebAuthnException(WebAuthnErrorCode.STATUS_RPID_VALIDATION_FAILED);
            }

            if(Strings.isNotBlank(verification) && verification.equals(WebAuthnConstants.WEBAUTH_USER_VERIFICATION_REQUIRED)){
                byte flags = authenticatorData.getFlags();
                String flagsStr = Integer.toBinaryString(flags);
                String uv = flagsStr.substring(2,3);
                if(!"1".equals(uv)){
                    LOG.error("Failed to validate uv in AuthenticatorData. flags:"+flags+",flagsStr:"+flagsStr);
                    throw new WebAuthnException(WebAuthnErrorCode.STATUS_UV_VALIDATION_FAILED);
                }
            }


            //验证模式判断
            if (Boolean.parseBoolean(strictmode)) {

//                    if (Boolean.parseBoolean(anonymous)) {
//                        if (!aaguid.normalizeCompare(WebAuthnConstants.WEBAUTH_ANONYMOUS_AUTHENTICATOR)) {
//                            throw new PolicyNotFoundException(UAFErrorCode.PROTOCOL_POLICY_VALIDATION_FAILED);
//                        }
//                    } else {
                    if (!aaguid.normalizeCompare(WebAuthnConstants.WEBAUTH_ANONYMOUS_AUTHENTICATOR)) {
                    //检查AAGUID是否在策略中
                    Set<String> aaguids = WebAuthnConstants.getAaguidInPolicy(reqPayload.getContext().getAppID(), reqPayload.getContext().getAuthType(), reqPayload.getContext().getTransType());
                    if (aaguids.isEmpty() || !aaguids.contains(aaguid.toString())) {
                        throw new PolicyNotFoundException(UAFErrorCode.PROTOCOL_POLICY_VALIDATION_FAILED);
                    }
                }
                //}
            }

            authenticator.setAaguid(aaguid);

            WebAuthnConstants.AttestationResult attestationResult = WebAuthnConstants.AttestationResult.UNAVAILABLE;
            if (format != AttestationFormat.NONE)
            {
                LOG.debug("the att format : {}", format);
                if(AttestationFormat.PACKED.equals(format)){
                    PackedAttestationProcessor packedAttestationProcessor = new PackedAttestationProcessor(GuiceUtil.getProcessorInjector().getInstance(CryptoEngine.class));
                    packedAttestationProcessor.verify(reqPayload.getCredentials(), authenticator,Boolean.parseBoolean(strictmode));
                }else if(AttestationFormat.U2F.equals(format)){
                    U2FAttestationProcessor u2fAttestationProcessor = new U2FAttestationProcessor(GuiceUtil.getProcessorInjector().getInstance(CryptoEngine.class));
                    u2fAttestationProcessor.verify(reqPayload.getCredentials(), authenticator,Boolean.parseBoolean(strictmode));
                }else if(AttestationFormat.ANROID_KEY.equals(format)){
                    AndroidKeyAttestationProcessor androidKeyAttestationProcessor = new AndroidKeyAttestationProcessor(GuiceUtil.getProcessorInjector().getInstance(CryptoEngine.class));
                    androidKeyAttestationProcessor.verify(reqPayload.getCredentials(), authenticator,Boolean.parseBoolean(strictmode));
                }
            }
            conn = UAFDBConnectionMgr.getConnection(true);
            WebAuthnMetadataDAO webAuthnMetadataDAO = UAFDAOFactory.createWebAuthnMetadataDAO(conn);
            WebAuthnMetadata webAuthnMetadata = webAuthnMetadataDAO.findByAAGUID(authenticatorData.getAttestation().getAaguid().toString());
            if (webAuthnMetadata == null)
                authenticator.setWmId("00000000000000000000000000000000");//未知id
            else
                authenticator.setWmId(webAuthnMetadata.getWmId());

            // hash 用户名
            String hashedUserName = generateHashedUserName(reqPayload.getContext().getUserName(), reqPayload.getContext().getRf1(), reqPayload.getContext().getRf2());
            authenticator.setUserName(hashedUserName);
            authenticator.setCustNo(reqPayload.getContext().getUserName());

            authenticator.setUserId(options.getUser().getId());

            authenticator.setDisplayName(reqPayload.getContext().getUserName());

            AttestationFormat attFormat = credentialAttestation.getFormat();
            authenticator.setAttestationFormat(attFormat);

            authenticator.setTransports(new ArrayList<AuthenticatorTransport>());
            authenticator.setType(reqPayload.getCredentials().getType());
            authenticator.setCredentialId(authenticatorData.getAttestation().getCredentialID());
            authenticator.setPublicKey(authenticatorData.getAttestation().getCredentialPublicKey().getPublicKeyInRawFormat());

            authenticator.setKeyFormat(authenticatorData.getAttestation().getCredentialPublicKey().getPublicKeyFormat());

            SignAlgorithmIdentifier algorithmIdentifier = authenticatorData.getAttestation().getCredentialPublicKey().getAlg();
            authenticator.setSignAlgo(algorithmIdentifier.getSignAlgID());

            authenticator.setRegCounter(0);
            authenticator.setSignCounter(authenticatorData.getSignCount());
            authenticator.setStatus(1);
            authenticator.setAuthCounter(0);
            authenticator.setAttestCertChainHash("");
            String deviceID = " ";
            if(null != reqPayload.getContext().getDevices()){
                if(null != reqPayload.getContext().getDevices().getDeviceID()){
                    deviceID = reqPayload.getContext().getDevices().getDeviceID();
                }
            }
            Calendar calendar = Calendar.getInstance();
            Timestamp currentTimestamp = new Timestamp(calendar.getTime().getTime());
            authenticator.setCreateTimeStamp(currentTimestamp);
            authenticator.setUpdateTimeStamp(currentTimestamp);
            authenticator.setDeviceId(deviceID);
            authenticator.setTenantId(reqPayload.getContext().getAppID());
            authenticator.setAuthType(reqPayload.getContext().getAuthType());
            authenticator.setTransType(reqPayload.getContext().getTransType());
            authenticator.setRf1(reqPayload.getContext().getRf1());
            authenticator.setRf2(reqPayload.getContext().getRf2());

            saveConn = UAFDBConnectionMgr.getConnection(false);
            WebAuthnAuthenticatorsDAO webauthnAuthenticatorsDAO = UAFDAOFactory.createWebAuthnAuthenticatorsDAO(saveConn);
            webauthnAuthenticatorsDAO.insert(authenticator);
            //保存设备信息
            //browesDetails
            DeviceDetails deviceDetails = new DeviceDetails();
            deviceDetails.setAppID(reqPayload.getContext().getAppID());
            deviceDetails.setUserName(reqPayload.getContext().getUserName());

            deviceDetails.setDeviceID(deviceID);
            deviceDetails.setAuthType(reqPayload.getContext().getAuthType());
            deviceDetails.setTransType(reqPayload.getContext().getTransType());
            deviceDetails.setStatus(Constants.AuthenticatorStatus.ACTIVE.getStatus());
            if(null != reqPayload.getContext().getDevices() && null != reqPayload.getContext().getDevices().getDeviceInfo()) {
                UserAgentInfo userAgentInfo = UserAgentUtil.uasParser.parse(new String(Convert.fromBase64(reqPayload.getContext().getDevices().getDeviceInfo())));
                deviceDetails.setOsType(userAgentInfo.getOsFamily());
                deviceDetails.setOsVersion(userAgentInfo.getOsName());
                deviceDetails.setDeviceName(userAgentInfo.getBrowserVersionInfo());
                deviceDetails.setDeviceType(userAgentInfo.getUaFamily());
            }
            saveDevice(deviceDetails, saveConn);
            saveConn.commit();
            respPayload.setStatusCode(Constants.StatusCode.OK.getCode());
            respPayload.setDescription("OK");
            response = gson.toJson(respPayload);


        } catch (Exception ex) {
            DBUtil.rollback(saveConn);
            LOG.error("[/reg/send] ERROR", ex);
            response = buildWebAuthnRespFromException(respPayload, ex);
        }finally {
            DBUtil.close(conn);
            DBUtil.close(saveConn);
        }

        LOG.info("[/reg/send][end]");
        return response;

    }

    private void validate(WebAuthnReqPayload<WebAuthnRegContext> reqPayload) throws PayloadValidationException, UnsupportedEncodingException {
        if ((reqPayload == null) || (reqPayload.getContext() == null)) {
            throw new PayloadValidationException("context", RestValidator.VALIDATION_ERROR.EMPTY);
        }
        RestValidator.validate("userName", reqPayload.getContext().getUserName());
        RestValidator.validate("appID", reqPayload.getContext().getAppID());
        //RestValidator.validate("deviceID", reqPayload.getContext().getDevices().getDeviceID());
        RestValidator.validate("authType", reqPayload.getContext().getAuthType());
        RestValidator.validate("transType", reqPayload.getContext().getTransType());
        RestValidator.validate("transNo", reqPayload.getContext().getTransNo());
        RestValidator.validate("rf1", reqPayload.getContext().getRf1());
        RestValidator.validate("rf2", reqPayload.getContext().getRf2());
        RestValidator.validate("protocol", reqPayload.getContext().getProtocol());
    }

    protected void saveDevice(DeviceDetails a, Connection conn) throws DAOException {

        DevicesDao devdao = UAFDAOFactory.createDevicesDAO(conn);
        DeviceDetails deviceDetails = devdao.findOne(a.getAppID(), a.getUserName(), a.getDeviceID(), a.getAuthType(), a.getTransType());
        if (deviceDetails != null) {
            deviceDetails.setStatus(Constants.AuthenticatorStatus.ACTIVE.getStatus());
            devdao.update(deviceDetails);
        } else {
            devdao.insert(a);
        }

    }


}
